mod yandex;

use std::{sync::Arc, time::Duration};
use chrono::{DateTime, Utc};

use ::oauth2::{
    basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, 
    PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, TokenUrl, AccessToken, IntrospectionUrl
};

use serde::Deserialize;
pub use crate::store::UserData;
use crate::store::*;
use crate::*;

const DEFAULT_TTL: Duration = Duration::from_secs(3600*24*365);
//const REFRESH_PERIOD: Duration = Duration::from_secs(3600*24);


#[derive(Debug, Deserialize)]
pub struct OAuth2Config {
    #[serde(default)]
    pub client_id: String,
    #[serde(default)]
    pub client_secret: String,
    pub auth_url: String,       //https://oauth.yandex.ru/authorize
    pub token_url: String,      //https://oauth.yandex.ru/token
    pub redirect_url: String,   //https://rc2.moiolvil.ru/token
    pub scopes: Vec<String>,    //"scope": ["login:email", "login:default_phone"], 
    pub introspection_url: String,
    pub introspection_api: IntrospectionApi,   //Standard, Yandex
}

impl OAuth2Config {
    pub fn load_from_environment() -> Result<Self> {
        fn var(name: &'static str) -> Result<String> { std::env::var(name).ok().ok_or_else(|| AuthError::NoEnvVar(name)) }
        let client_id = var("OAUTH2_CILENT_ID")?;
        let client_secret = var("OAUTH2_CILENT_SECRET")?;
        let auth_url = var("OAUTH2_URL")?;
        let token_url = var("OAUTH2_TOKEN_URL")?;
        let redirect_url = var("OAUTH2_REDIRECT_URL")?;
        let scopes = var("OAUTH2_SCOPES")?.split(',').map(|w| w.to_owned()).collect();
        let introspection_url = var("OAUTH2_INTRO_URL")?;
        let introspection_api: IntrospectionApi = var("OAUTH2_INTRO_API")?.parse()?;
        Ok(Self { client_id, client_secret, auth_url, token_url, redirect_url, scopes, introspection_url, introspection_api })
    }

    pub fn update_from_environment(mut self) -> Result<Self> {
        fn set_var<T>(val: &mut T, name: &'static str, c: impl Fn(String) -> Result<T>) -> Result<()> { 
            if let Some(value) = std::env::var(name).ok() { *val = c(value)?; }
            Ok(()) 
        }
        fn id(w: String) -> Result<String> { Ok(w) }
        set_var(&mut self.client_id, "OAUTH2_CILENT_ID", id)?;
        set_var(&mut self.client_secret, "OAUTH2_CILENT_SECRET", id)?;
        set_var(&mut self.auth_url, "OAUTH2_URL", id)?;
        set_var(&mut self.token_url, "OAUTH2_TOKEN_URL", id)?;
        set_var(&mut self.redirect_url, "OAUTH2_REDIRECT_URL", id)?;
        set_var(&mut self.scopes, "OAUTH2_SCOPES", |w| Ok(w.split(',').map(|w| w.to_owned()).collect()))?;
        set_var(&mut self.introspection_url, "OAUTH2_INTRO_URL", id)?;
        set_var(&mut self.introspection_api, "OAUTH2_INTRO_API", |w| w.parse())?;
        Ok(self)
    }
}


pub struct ValidationInfo {
    pub return_uri: Option<String>,
}

#[derive(Deserialize, Debug, Clone)]
pub enum IntrospectionApi {
    /// rfc7662 section-2.2
    #[serde(rename="standard")]
    Standard,

    /// Yandex (JSON format)
    #[serde(rename="yandex")]
    Yandex,
}

impl std::str::FromStr for IntrospectionApi {
    type Err = crate::AuthError;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s {
            "standard" => Ok(Self::Standard),
            "yandex" => Ok(Self::Yandex),
            o => Err(AuthError::InvalidIntrospectionApi(o.to_owned()))
        }
    }
}

#[derive(Clone)]
pub struct OAuth2 {
    client: BasicClient,
    scopes: Vec<Scope>,
    auth_store: Arc<dyn AuthStore>,
    introspection_api: IntrospectionApi,
}


impl OAuth2 {
    pub fn new(config: OAuth2Config, auth_store: Arc<dyn AuthStore>) -> Result<Self> {
        let client =
            BasicClient::new(
                ClientId::new(config.client_id),
                Some(ClientSecret::new(config.client_secret)),
                AuthUrl::new(config.auth_url)?,
                Some(TokenUrl::new(config.token_url)?)
            )
            // Set the URL the user will be redirected to after the authorization process.
            .set_redirect_uri(RedirectUrl::new(config.redirect_url)?)
            .set_introspection_uri(IntrospectionUrl::new(config.introspection_url)?);        

        let scopes = config.scopes.into_iter().map(Scope::new).collect();

        let introspection_api = config.introspection_api;

        Ok(Self { client, auth_store, scopes, introspection_api })
    }

    pub async fn redirect(&self, uid: &str, return_uri: Option<String>) -> Result<String> {
        // Generate a PKCE challenge.
        let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

        // Generate the full authorization URL.
        let url = self.client
            .authorize_url(CsrfToken::new_random);

        // Set the desired scopes.
        let url = self.scopes.iter().cloned().fold(url, |u, s| u.add_scope(s));

        let (auth_url, csrf_token) = url
            // Set the PKCE code challenge.
            .set_pkce_challenge(pkce_challenge)
            .url();

        self.auth_store.set_auth(uid, AuthInfo { csrf_token, pkce_verifier, return_uri }).await;

        // This is the URL you should redirect the user to, in order to trigger the authorization
        // process.
        Ok(auth_url.to_string())
    }

    fn handle_ttl(&self, ttl: Option<Duration>) -> DateTime<Utc> {
        let ttl = ttl.unwrap_or(DEFAULT_TTL);
        let expires = chrono::Utc::now() + ttl;
        expires
    }

    /// performs exchange-code-for-token; stores the received token in the internal storage
    pub async fn validate(&self, uid: &str, code: String, state: String) -> Result<ValidationInfo> {
        // Once the user has been redirected to the redirect URL, you'll have access to the
        // authorization code. For security reasons, your code should verify that the `state`
        // parameter returned by the server matches `csrf_state`.

        let Some(AuthInfo { 
            csrf_token, 
            pkce_verifier, 
            return_uri
        }) = self.auth_store.remove_auth(uid).await else {
            return Err(AuthError::PreAuthSessionNotFound)
        };

        // check CSRF token (expected as state)
        if csrf_token.secret() != &state {
            return Err(AuthError::CsrfTokenMismatch);
        }

        // Now you can trade it for an access token.
        // Unwrapping token_result will either produce a Token or a RequestTokenError.
        let token_result = self.client
            .exchange_code(AuthorizationCode::new(code))
            // Set the PKCE code verifier.
            .set_pkce_verifier(pkce_verifier)
            .request_async(async_http_client)
            .await.map_err(|e| AuthError::RequestTokenError(Box::new(e)))?;

        let access_token = token_result.access_token().clone();
        let refresh_token = token_result.refresh_token().cloned();
        let expires = self.handle_ttl(token_result.expires_in());
        
        let user_data = self.fetch_user_data(&access_token).await?;

        self.auth_store.set_token_info(uid, TokenInfo { access_token, refresh_token, expires, user_data }).await;

        Ok(ValidationInfo { return_uri })
    }

    pub async fn refresh(&self, uid: &str) -> Result<()> {
        let d = self.auth_store.get_token_info(uid).await.ok_or(AuthError::NotLoggedIn)?;
        let Some(refresh_token) = d.refresh_token.as_ref() else { return Ok(()) };
        if Utc::now() < d.expires { return Ok(()); }

        let token_result = self.client
            .exchange_refresh_token(refresh_token)
            .request_async(async_http_client)
            .await.map_err(|e| AuthError::RequestTokenError(Box::new(e)))?;
        let access_token = token_result.access_token().clone();
        let refresh_token = token_result.refresh_token().cloned();
        let expires = self.handle_ttl(token_result.expires_in());

        let user_data = self.fetch_user_data(&access_token).await?;
        
        self.auth_store.set_token_info(uid, TokenInfo { access_token, refresh_token, expires, user_data }).await;
        Ok(())
    }

    async fn fetch_user_data_standard(&self, access_token: &AccessToken) -> Result<UserData> {
        let _resp = self.client
            .introspect(access_token)?
            .request_async(async_http_client)
            .await.map_err(|e| AuthError::RequestTokenError(Box::new(e)))?;
        todo!()
    }

    async fn fetch_user_data_yandex(&self, access_token: &AccessToken) -> Result<UserData> {
        yandex::TokenIntrospectionRequest::new(self, access_token)
            .request_async(async_http_client).await
    }

    async fn fetch_user_data(&self, access_token: &AccessToken) -> Result<UserData> {
        match self.introspection_api {
            IntrospectionApi::Standard => self.fetch_user_data_standard(access_token).await,
            IntrospectionApi::Yandex => self.fetch_user_data_yandex(access_token).await,
        }
    }

    pub async fn user_data(&self, uid: &str) -> Option<UserData> {
        self.auth_store.get_token_info(uid).await.map(|d| d.user_data)
    }

}




